package cn.handong.tool.algorithm.背包问题;

import org.junit.Test;

import java.util.Arrays;

public class Knapsack {
    public static void main(String[] args) {

    }

    @Test
    public void backpack05(){
        int N = 5;  //物品的数目 N
        int V = 10;//背包的体积 V
        int[] v = {0,6,4,2,4,3}; // 体积 一个长度为N的数组，第i个元素表示第i个物品的体积；从1开始
        int[] w = {0,8,10,4,5,5}; // 价值 一个长度为N的数组，第i个元素表示第i个物品的价值；从1开始
        int[][] dp = new int[N+1][V+1]; // dp[i][j]表示前i个物品在容量为j时可获取的最大价值
        dp[0][0] = 0;
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= V; j++) {
                if (j > v[i]){
                    dp[i][j] = Math.max(dp[i][j-v[i]]+w[i], // 加了物品i
                                        dp[i-1][j]);        // 不加物品i
                }else {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        System.out.println(dp[N][V]);
    }


    /**
     * 01背包问题
     * 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
     * 第 i 件物品的体积是 vi，价值是 wi。
     * 求解将哪些物品装入背包，可使这些物品的总体积不超过背包容量，且总价值最大。输出最大价值。
     * 定义一个二阶矩阵dp[N+1][V+1],
     * dp[i][j]表示在 只能选择前i个物品，背包容量为j的情况下，背包中物品的最大价值。
     * 这里之所以要N+1和V+1，是因为第0行表示只能选择第0个物品的时候，即没有物品的时候第0列表示背包的体积为0的时候，即不能装任何东西的时候.
     *
     * 已知dp[i-1][j],要求dp[i][j],则需要状态转移方程
     * 对于dp[i][j]有两种情况：
     *
     * 　1.不选择当前的第i件物品/第i件物品比背包容量要大
     * 　　　　　　　　　　　　dp[i][j] = dp[i-1][j]
     * 　　2.选择当前的第i件物品（潜在要求第i件物品体积小于等于背包总容量），则能装入的物品最大价值为：当前物品的价值 加上 背包剩余容量在只能选前i-1件物品的情况下的最大价值。表示为：
     * 　　　　　　　　　　dp[i][j] = dp[i-1][j-v[i]] + w[i]
     * dp[i][j]在两种情况中选择比较大的情况作为当前的最优解；
     * 即：
     * 　　　if(j >= v[i]):
     * 　　　　　　dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i])
     * 　　　else:
     * 　　　　　　dp[i][j] = dp[i-1][j]
     *
     * 以上的动态规划需要一个二维的dp空间。我们可以优化为一个一维的dp数组。此时，dp[v]就表示在v大小的背包空间下，可以放入的最大的物品价值 。此时的物品依次放入。当我们已知第i-1个物品放入后，v的最大值，那么加入第i个物品时，最大值要么是放入第i个物品，要么是原来的值。表达式：
     * 　　　　　　dp[v] = Math.max( dp[v - v[i] ) + w[i] , dp[v])
     * 参考代码如下：
     */
    @Test
    public void backpack01(){
        int N = 5;  //物品的数目 N
        int V = 10;//背包的体积 V
        int[] v = {0,6,4,2,4,3}; // 体积 一个长度为N的数组，第i个元素表示第i个物品的体积；从1开始
        int[] w = {0,8,10,4,5,5}; // 价值 一个长度为N的数组，第i个元素表示第i个物品的价值；从1开始
        int[][] dp = new int[N+1][V+1]; //dp[i][j]表示在 只能选择前i个物品，背包容量为j的情况下，背包中物品的最大价值
        dp[0][0] = 0;
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= V; j++) {
                if (j >= v[i]){  //此时背包容量j >= 第i个物品的体积v[i] 可以放进去
                    dp[i][j] = Math.max(dp[i-1][j],           //不装第i件物品
                                        dp[i-1][j-v[i]]+w[i]);//装第i件物品
                }else {    //装不下第i件物品
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        System.out.println("最大价值为："+dp[N][V]);

        //压缩为一维数组
        int[] dps = new int[V+1];
        dps[0] = 0;
        for (int i = 1; i <= N; i++) {
            for (int j = V; j >= v[i]; j--) {
                dps[j] = Math.max(dps[j],dps[j-v[i]]+w[i]);
            }
        }
        System.out.println("最大值为："+dps[V]);
    }
    /**
     * 测试一下手写01背包
     * 0,6,4,2,4,3    v
     * 0,8,10,4,5,5   w
     */
    @Test
    public void backpack(){
        int N = 5;  //物品的数目 N
        int V = 10;//背包的体积 V
        int[] v = {0,6,4,2,4,3}; // 体积 一个长度为N的数组，第i个元素表示第i个物品的体积；从1开始
        int[] w = {0,8,10,4,5,5}; // 价值 一个长度为N的数组，第i个元素表示第i个物品的价值；从1开始
        int[][] dp = new int[N+1][V+1]; //dp[i][j] 表示前i个物品再体积魏j的情况下获得的最大价值
        dp[0][0] = 0; //
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= V; j++) {
                if (j >= v[i]){ //体积为j >= 第i个物品的体积v[i]
                    dp[i][j] = Math.max(dp[i-1][j],//不放入物品i
                            dp[i-1][j-v[i]]+w[i]);//在不放物品i且体积为j-v[i]时的最大价值 + 第i个物品的价值
                }else {
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        for (int[] ints : dp) {
            System.out.println(Arrays.toString(ints));
        }
//        System.out.println(Arrays.deepToString(dp));
        System.out.println(dp[N][V]);
    }


    /**
     * 完全背包问题
     *
     * 有 N 种物品和一个容量是 V 的背包，每种物品都有无限件可用。
     * 第 i 种物品的体积是 vi，价值是 wi。
     * 求解将哪些物品装入背包，可使这些物品的总体积不超过背包容量，且总价值最大。输出最大价值。
     * 可以模仿01背包，dp[i][j]记录前i个物品放入j空间的最大值。
     * 此时，由于一个物品可是使用无限次数，dp[i][j]应该从dp[i][j-v[i]]来而不是dp[i-1][j-v[i]]来。
     * 表达式为
     * dp[i][j]=Math.max(dp[i][j-v[i]]+w[i],dp[i-1][j])
     * 同样，完全背包问题一样可以优化至一维dp空间。
     * 01背包和完全背包使最常用的背包问题。很多其他背包问题可以转换为这两个问题来思考。
     * 参考代码如下：
     */
    @Test
    public void backpach02(){
        int[] v = {0,6,4,2,4,3}; // 体积 一个长度为N+1的数组，第i个元素表示第i个物品的体积；从1开始
        int[] w = {0,8,10,4,5,5}; // 价值 一个长度为N+1的数组，第i个元素表示第i个物品的价值；从1开始
        int N = 5;  // 有 N 种物品
        int V = 10; // 容量是 V 的背包
//        int[][] dp = new int[N+1][V+1];//dp[i][j]记录前i个物品放入j空间的最大值。
//        for (int i = 1; i <= N; i++) {  //前i个物品  从 1 到 N
//            for (int j = 0; j <= V; j++) { //体积只有j时 从 0 到 V
//                if (j >= v[i]){  //此时空间大于第i个物品体积，可以放进去
//                    dp[i][j] = Math.max(dp[i][j-v[i]]+w[i], //第i个物品放进去了
//                                        dp[i-1][j]);        //第i个物品不放进去
//                }else {
//                    dp[i][j] = dp[i-1][j]; //第i个物品没放进去
//                }
//            }
//        }
//        System.out.printf("最大值为："+dp[N][V]);

//        int[] dp = new int[V+1]; //dp[v]表示在v空间下装入的最大价值
//        for (int i = 1; i <= N; i++) { //前i个物品  从 1 到 N
//            for (int j = V; j >= v[i]; j++) { // 体积 j 从 V 到 v[i] ，不能低于v[i]，因为低于了就不会放这个，不放的话，和前一个i一样
//                for (int k = 0; j - k * v[i] >= 0; k++) {
//                    dp[j] = Math.max(dp[j],                //之前的dp
//                                     dp[j-k*v[i]]+k*w[i]); //加入k个v[i]后的dp
//                }
//            }
//        }
//        System.out.println(dp[V]);


        //和01背包相比在于01背包从后向前遍历，由于使用到之前的状态，从后向前时前面的状态为0，确保了一个物品只使用了一次。
        //完全背包使用从前向后遍历，前面的状态先遍历。此时后面的状态再计算时，使第i个物品重复使用。
        int[] dp = new int[V+1];
        for (int i = 1; i <= N; i++) {
            for (int j = v[i]; j <= V; j++) {
                dp[j] = Math.max(dp[j],           //前一个的dp
                                 dp[j-v[i]]+w[i]);//后来的dp
            }
        }
        System.out.println(dp[V]);
    }


    /**
     * 多重背包问题
     *
     * 有 N 种物品和一个容量是 V 的背包。
     * 第 i 种物品最多有 si 件，每件体积是 vi，价值是 wi。
     * 求解将哪些物品装入背包，可使物品体积总和不超过背包容量，且价值总和最大。
     * 思路：
     * 　　参考完全背包问题，就是将完全背包中的数量限制改为si. （体现在for循环中）
     * 优化：
     * 　　利用二进制，将背包数量分解，变为01背包问题。
     */
    @Test
    public void backpack03(){
        int[] v = {0,6,4,2,4,3}; // 体积 一个长度为N+1的数组，第i个元素表示第i个物品的体积；从1开始
        int[] w = {0,8,10,4,5,5}; // 价值 一个长度为N+1的数组，第i个元素表示第i个物品的价值；从1开始
        int[] s = {0,3,2,1,6,5}; //  数量 一个长度为N+1的数组，第i个元素表示第i个物品的数目；从1开始
        int N = 5;  // 有 N 种物品
        int V = 10; // 容量是 V 的背包
        int[] dp = new int[V+1];//
        for (int i = 1; i <= N; i++) {
            for (int j = V; j >= v[i]; j++) {
                for (int k = 1; j - k * v[i] >= 0 && k <= s[i]; k++) {
                    dp[j] = Math.max(dp[j],dp[j-k * v[i]] + k * w[i]);
                }
            }
        }
        System.out.println(dp[V]);
    }



}
